Skip to content

Document the threaded @batch per-block allocation and its root cause (#198)#199

Merged
zsoerenm merged 1 commit into
masterfrom
fix-198
Jul 9, 2026
Merged

Document the threaded @batch per-block allocation and its root cause (#198)#199
zsoerenm merged 1 commit into
masterfrom
fix-198

Conversation

@zsoerenm

@zsoerenm zsoerenm commented Jul 9, 2026

Copy link
Copy Markdown
Member

Closes #198. Documentation-only, on top of #197 (merged), which is the substantive fix. This PR corrects docstrings that mislabeled the threaded allocation, and records the root-cause analysis + prototypes from investigating it.

What #198 turned out to be

  1. The reported "~40 KB per completed integration" is a measurement artifact. The reproducer measures @allocated track!(rand(ComplexF32, n), …) with n = k·5000; the rand input buffer sits inside the @allocated block and is exactly n·8 B = 40 KB per 5000-sample code period, so it grows with completions. Hoisted out and measured from a typed call site, track! is flat 0 B across 1→100 completions, 1 and 11 sats, pre- and post-sync.

  2. perf(bit-buffer): fix pre-sync per-code-block allocation in track! (re-target of #194 to master) #197 is the real fix. _buffer_find_bit's Core.Box leaked ~80 B/block (proportional to signal length — exactly the reported symptom). It reproduces on the single-threaded backend (160 B @ 2 blocks → 1600 B @ 20) at any runtime thread count, so the pre-sync guard added in perf(bit-buffer): fix pre-sync per-code-block allocation in track! (re-target of #194 to master) #197 catches it. With perf(bit-buffer): fix pre-sync per-code-block allocation in track! (re-target of #194 to master) #197 the single-threaded path is 0 B.

  3. One genuine residual remains, on the threaded backend: ~64 B per completed code block per group, but only with >1 thread AND >1 satellite (so Polyester's @batch actually distributes work). track! launches one @batch per code block, so it scales with chunk length rather than per call — though it's bounded per call and dwarfed by the input buffer.

Root cause (now documented)

Polyester's @batch roots only bare Arrays and isbits values into its worker tasks for free (that's why a Vector{Float64} kernel is allocation-free). It pays a small per-launch allocation to root any non-isbits struct — and it's the struct-ness that costs, not the contents: capturing a plain struct whose only fields are Matrixes + isbits scalars measures the same ~64 B/launch, while capturing those same bare arrays measures 0 (verified).

The culprit is the GNSS signal: GPSL1CA is not isbits — it wraps a Matrix{Int16} code table and a (also non-isbits) SignalLUT. Each satellite's code-replica generation needs its signal, so every @batch launch touches that non-isbits object once. (The TrackedSat element is non-isbits for the same reason and is how the loop reaches the signal, so iterating Vector{TrackedSat} isn't free the way iterating Vector{Float64} is.)

Prototypes explored (not included — investigation only)

Two ways to reach 0 B were prototyped and measured:

approach allocation code-gen throughput vs current
serial code-gen pre-pass + parallel signal-free fused correlate 0 B serial slower — ~5–9% (2 thr), ~20% (4 thr), ~35–45% (8 thr); serializing ~30% of the work caps parallel speedup (Amdahl)
bare-array gen_code! (gen from the raw padded matrix, inside the loop) 0 B parallel throughput-neutral in principle (same work); not speed-benchmarked — prototype gen is scalar, not SIMD

Key findings:

  • Bit-faithful in both cases (Δ = 0 vs the current per-sat path; the bare-array gen matched the real gen_code! exactly in 3/4 spot checks, off by 1 sample in the 4th due to fixed-point-DDA rounding).
  • No speedups were found. The serial approach is slower; the bare-array approach is at best throughput-neutral. (Microbenchmarks are warm/min-time and exclude GC, so any long-run GC-pressure benefit from 0 B is unmeasured — and likely negligible next to the input buffer.)
  • Hoisting the SignalLUT out of GPSL1CA does not help by itself (it's still a struct). Reaching 0 without a throughput hit needs a GNSSSignals-side bare-array gen_code! that threads the LUT's arrays + isbits fields to the SIMD resample kernel as separate arguments (never re-wrapped in a struct inside the loop — doing so hits a type-inference cliff and allocates far more). That's a cross-package API change for ~64 B/block, so it's left as a documented follow-up, not done here.

Changes in this PR

The track! benchmark size is intentionally left at 2000 samples (no change).

🤖 Generated with Claude Code

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.63%. Comparing base (99d37e1) to head (16f259c).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #199      +/-   ##
==========================================
- Coverage   97.87%   97.63%   -0.25%     
==========================================
  Files          32       32              
  Lines        3296     3296              
==========================================
- Hits         3226     3218       -8     
- Misses         70       78       +8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (minimum time) — ubuntu-latest

Reporting the minimum over all samples (robust to shared-runner contention), not the median.

Alternative backends vs Float32 (track!, PR head)

Legend — backends: F32 Float32 (default) · I16 Int16 · 1b OneBit · 2b TwoBit (2-bit measurement + 2-bit carrier). Time columns are the minimum track! time; ×B = F32 / B (so >1 ⇒ backend B is faster than Float32), ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. 1b/2b are BPSK-only, so their cells are blank for CBOC (Galileo E1B) scenarios.

Scenario F32 I16 1b 2b ×I16 ×1b ×2b
4-antenna @ 5 MHz 12.2 μs 6.09 μs 5.23 μs 9.74 μs 2.01 ✅ 2.34 ✅ 1.26 ✅
GPS L1CA, 8 sats @ 40 MHz 331.0 μs 136.0 μs 71.1 μs 120.0 μs 2.43 ✅ 4.66 ✅ 2.77 ✅
GPS L1CA, 8 sats @ 5 MHz 49.1 μs 28.2 μs 20.1 μs 27.9 μs 1.74 ✅ 2.44 ✅ 1.76 ✅
Galileo E1B, 4 sats @ 25 MHz 123.0 μs 55.2 μs 2.22 ✅
dynamic taps @ 5 MHz (kernel) 5.78 μs 3.4 μs 2.04 μs 2.81 μs 1.7 ✅ 2.84 ✅ 2.06 ✅
multi-signal N=3 @ 5 MHz 9.53 μs 5.84 μs 5.01 μs 7.09 μs 1.63 ✅ 1.9 ✅ 1.34 ✅
Time benchmarks (base vs PR head)

Ratio = 99d37e1… / 16f259c…: >1 means the PR is faster. ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. A blank cell means the benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

99d37e1 16f259c 99d37e1… / 16f259c
downconvert and correlate/CPU/Float32 2.34 μs 2.34 μs 0.998
downconvert and correlate/CPU/Float32 4ant 4.72 μs 4.74 μs 0.995
downconvert and correlate/CPU/Float64 2.76 μs 2.76 μs 0.998
downconvert and correlate/CPU/Int16 2.42 μs 2.42 μs 1.0
downconvert and correlate/CPU/Int16 4ant 5.19 μs 5.2 μs 0.997
downconvert and correlate/CPU/Int32 2.41 μs 2.41 μs 0.998
fused kernel/1-ant dynamic taps 2.16 μs 2.16 μs 1.0
fused kernel/1-ant static taps 1.88 μs 1.88 μs 1.0
fused kernel/4-ant dynamic taps 5.44 μs 5.36 μs 1.01
fused kernel/4-ant static taps 4.13 μs 4.16 μs 0.994
fused tuple kernel/1-ant N=2 2.35 μs 2.35 μs 1.0
fused tuple kernel/1-ant N=3 2.85 μs 2.82 μs 1.01
fused tuple kernel/2-ant N=2 3.73 μs 3.6 μs 1.04
fused tuple kernel/2-ant N=3 4.58 μs 4.55 μs 1.01
fused tuple kernel/4-ant N=2 6.0 μs 6.11 μs 0.981
fused tuple kernel/4-ant N=3 9.73 μs 9.78 μs 0.995
track/1. Float32/2K – track 2.57 μs 2.57 μs 1.0
track/1. Float32/2K – track! 2.66 μs 2.65 μs 1.0
track/2. L1 8sat/5K – track 48.0 μs 47.7 μs 1.01
track/2. L1 8sat/5K – track! 47.3 μs 47.3 μs 1.0
track/2. L1 8sat/5K – track! Int16 29.5 μs 27.9 μs 1.06 ✅
track/2. L1 8sat/5K – track! OneBit 20.1 μs 20.1 μs 1.0
track/2. L1 8sat/5K – track!-threaded 47.4 μs 47.1 μs 1.01
track/2. L1 8sat/5K – track-threaded 48.0 μs 48.7 μs 0.986
track/3. E1B 4sat/25K – track 119.0 μs 118.0 μs 1.01
track/3. E1B 4sat/25K – track! 118.0 μs 117.0 μs 1.01
track/3. E1B 4sat/25K – track! Int16 55.3 μs 54.9 μs 1.01
track/3. E1B 4sat/25K – track!-threaded 118.0 μs 117.0 μs 1.01
track/3. E1B 4sat/25K – track-threaded 119.0 μs 118.0 μs 1.01
track/4. 8L1+8E1B/25K – track 436.0 μs 435.0 μs 1.0
track/4. 8L1+8E1B/25K – track! 435.0 μs 432.0 μs 1.01
track/4. 8L1+8E1B/25K – track!-threaded 435.0 μs 434.0 μs 1.0
track/4. 8L1+8E1B/25K – track-threaded 436.0 μs 435.0 μs 1.0
track/5. multi-signal N=1/5K – track 6.14 μs 6.09 μs 1.01
track/5. multi-signal N=1/5K – track! 5.89 μs 6.31 μs 0.934 ⚠️
track/6. multi-signal N=2/5K – track 8.37 μs 8.35 μs 1.0
track/6. multi-signal N=2/5K – track! 8.51 μs 8.5 μs 1.0
track/7. multi-signal N=3/5K – track 10.7 μs 10.7 μs 1.01
track/7. multi-signal N=3/5K – track! 11.0 μs 10.9 μs 1.01
track/8. L1CA presync 2 blk – track! 11.6 μs 11.6 μs 1.01
track/8. L1CA presync 20 blk – track! 113.0 μs 113.0 μs 1.0
track/8. L1CA synced 2 blk – track! 11.6 μs 11.5 μs 1.01
track/8. L1CA synced 20 blk – track! 112.0 μs 111.0 μs 1.01
time_to_load 99.4 μs 109.0 μs 0.913 ⚠️
Memory benchmarks (base vs PR head)
99d37e1 16f259c
downconvert and correlate/CPU/Float32 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Float32 4ant 2 allocs: 848 B 2 allocs: 848 B
downconvert and correlate/CPU/Float64 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Int16 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Int16 4ant 2 allocs: 848 B 2 allocs: 848 B
downconvert and correlate/CPU/Int32 2 allocs: 576 B 2 allocs: 576 B
fused kernel/1-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/1-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=3 0 allocs: 0 B 0 allocs: 0 B
track/1. Float32/2K – track 9 allocs: 944 B 9 allocs: 944 B
track/1. Float32/2K – track! 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track 10 allocs: 4536 B 10 allocs: 4536 B
track/2. L1 8sat/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track! Int16 13 allocs: 1056 B 13 allocs: 1056 B
track/2. L1 8sat/5K – track! OneBit 45 allocs: 2736 B 45 allocs: 2736 B
track/2. L1 8sat/5K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track-threaded 10 allocs: 4536 B 10 allocs: 4536 B
track/3. E1B 4sat/25K – track 10 allocs: 2808 B 10 allocs: 2808 B
track/3. E1B 4sat/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track! Int16 5 allocs: 416 B 5 allocs: 416 B
track/3. E1B 4sat/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track-threaded 10 allocs: 2808 B 10 allocs: 2808 B
track/4. 8L1+8E1B/25K – track 26 allocs: 10352 B 26 allocs: 10352 B
track/4. 8L1+8E1B/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track-threaded 26 allocs: 10352 B 26 allocs: 10352 B
track/5. multi-signal N=1/5K – track 9 allocs: 944 B 9 allocs: 944 B
track/5. multi-signal N=1/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/6. multi-signal N=2/5K – track 9 allocs: 1408 B 9 allocs: 1408 B
track/6. multi-signal N=2/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/7. multi-signal N=3/5K – track 9 allocs: 1760 B 9 allocs: 1760 B
track/7. multi-signal N=3/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/8. L1CA presync 2 blk – track! 7 allocs: 320 B 7 allocs: 320 B
track/8. L1CA presync 20 blk – track! 7 allocs: 320 B 7 allocs: 320 B
track/8. L1CA synced 2 blk – track! 7 allocs: 320 B 7 allocs: 320 B
track/8. L1CA synced 20 blk – track! 7 allocs: 320 B 7 allocs: 320 B
time_to_load 145 allocs: 11216 B 145 allocs: 11216 B

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (minimum time) — macos-14

Reporting the minimum over all samples (robust to shared-runner contention), not the median.

Alternative backends vs Float32 (track!, PR head)

Legend — backends: F32 Float32 (default) · I16 Int16 · 1b OneBit · 2b TwoBit (2-bit measurement + 2-bit carrier). Time columns are the minimum track! time; ×B = F32 / B (so >1 ⇒ backend B is faster than Float32), ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. 1b/2b are BPSK-only, so their cells are blank for CBOC (Galileo E1B) scenarios.

Scenario F32 I16 1b 2b ×I16 ×1b ×2b
4-antenna @ 5 MHz 11.8 μs 5.81 μs 3.97 μs 8.67 μs 2.04 ✅ 2.98 ✅ 1.37 ✅
GPS L1CA, 8 sats @ 40 MHz 394.0 μs 123.0 μs 64.2 μs 113.0 μs 3.21 ✅ 6.14 ✅ 3.49 ✅
GPS L1CA, 8 sats @ 5 MHz 56.6 μs 23.4 μs 16.9 μs 25.3 μs 2.42 ✅ 3.35 ✅ 2.24 ✅
Galileo E1B, 4 sats @ 25 MHz 141.0 μs 58.8 μs 2.4 ✅
dynamic taps @ 5 MHz (kernel) 7.17 μs 2.38 μs 1.55 μs 2.64 μs 3.02 ✅ 4.61 ✅ 2.71 ✅
multi-signal N=3 @ 5 MHz 11.2 μs 5.24 μs 3.87 μs 6.34 μs 2.15 ✅ 2.91 ✅ 1.77 ✅
Time benchmarks (base vs PR head)

Ratio = 99d37e1… / 16f259c…: >1 means the PR is faster. ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. A blank cell means the benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

99d37e1 16f259c 99d37e1… / 16f259c
downconvert and correlate/CPU/Float32 2.53 μs 2.48 μs 1.02
downconvert and correlate/CPU/Float32 4ant 4.86 μs 5.01 μs 0.971
downconvert and correlate/CPU/Float64 2.7 μs 2.8 μs 0.965
downconvert and correlate/CPU/Int16 2.62 μs 2.64 μs 0.991
downconvert and correlate/CPU/Int16 4ant 4.97 μs 4.82 μs 1.03
downconvert and correlate/CPU/Int32 2.71 μs 2.56 μs 1.06 ✅
fused kernel/1-ant dynamic taps 2.74 μs 2.74 μs 1.0
fused kernel/1-ant static taps 2.2 μs 2.2 μs 1.0
fused kernel/4-ant dynamic taps 8.33 μs 8.11 μs 1.03
fused kernel/4-ant static taps 4.75 μs 4.76 μs 0.999
fused tuple kernel/1-ant N=2 3.39 μs 3.51 μs 0.967
fused tuple kernel/1-ant N=3 3.65 μs 3.65 μs 1.0
fused tuple kernel/2-ant N=2 6.77 μs 6.76 μs 1.0
fused tuple kernel/2-ant N=3 6.02 μs 6.02 μs 1.0
fused tuple kernel/4-ant N=2 12.4 μs 12.4 μs 1.0
fused tuple kernel/4-ant N=3 10.8 μs 10.8 μs 1.0
track/1. Float32/2K – track 2.76 μs 2.77 μs 0.998
track/1. Float32/2K – track! 2.94 μs 2.84 μs 1.03
track/2. L1 8sat/5K – track 51.4 μs 53.3 μs 0.965
track/2. L1 8sat/5K – track! 52.6 μs 53.0 μs 0.993
track/2. L1 8sat/5K – track! Int16 21.3 μs 22.5 μs 0.948 ⚠️
track/2. L1 8sat/5K – track! OneBit 15.8 μs 16.4 μs 0.967
track/2. L1 8sat/5K – track!-threaded 49.3 μs 51.5 μs 0.959
track/2. L1 8sat/5K – track-threaded 53.2 μs 53.5 μs 0.995
track/3. E1B 4sat/25K – track 136.0 μs 140.0 μs 0.967
track/3. E1B 4sat/25K – track! 140.0 μs 140.0 μs 1.0
track/3. E1B 4sat/25K – track! Int16 60.2 μs 60.8 μs 0.991
track/3. E1B 4sat/25K – track!-threaded 136.0 μs 145.0 μs 0.934 ⚠️
track/3. E1B 4sat/25K – track-threaded 136.0 μs 140.0 μs 0.967
track/4. 8L1+8E1B/25K – track 490.0 μs 526.0 μs 0.932 ⚠️
track/4. 8L1+8E1B/25K – track! 506.0 μs 500.0 μs 1.01
track/4. 8L1+8E1B/25K – track!-threaded 524.0 μs 489.0 μs 1.07 ✅
track/4. 8L1+8E1B/25K – track-threaded 490.0 μs 507.0 μs 0.966
track/5. multi-signal N=1/5K – track 6.68 μs 6.88 μs 0.97
track/5. multi-signal N=1/5K – track! 6.8 μs 6.41 μs 1.06 ✅
track/6. multi-signal N=2/5K – track 11.2 μs 11.1 μs 1.0
track/6. multi-signal N=2/5K – track! 10.6 μs 10.5 μs 1.01
track/7. multi-signal N=3/5K – track 12.4 μs 12.4 μs 1.0
track/7. multi-signal N=3/5K – track! 11.7 μs 12.1 μs 0.965
track/8. L1CA presync 2 blk – track! 12.3 μs 13.3 μs 0.925 ⚠️
track/8. L1CA presync 20 blk – track! 120.0 μs 120.0 μs 1.0
track/8. L1CA synced 2 blk – track! 12.2 μs 12.7 μs 0.964
track/8. L1CA synced 20 blk – track! 127.0 μs 119.0 μs 1.07 ✅
time_to_load 153.0 μs 261.0 μs 0.585 ⚠️
Memory benchmarks (base vs PR head)
99d37e1 16f259c
downconvert and correlate/CPU/Float32 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Float32 4ant 2 allocs: 848 B 2 allocs: 848 B
downconvert and correlate/CPU/Float64 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Int16 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Int16 4ant 2 allocs: 848 B 2 allocs: 848 B
downconvert and correlate/CPU/Int32 2 allocs: 576 B 2 allocs: 576 B
fused kernel/1-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/1-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=3 0 allocs: 0 B 0 allocs: 0 B
track/1. Float32/2K – track 9 allocs: 944 B 9 allocs: 944 B
track/1. Float32/2K – track! 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track 10 allocs: 4656 B 10 allocs: 4656 B
track/2. L1 8sat/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track! Int16 13 allocs: 1056 B 13 allocs: 1056 B
track/2. L1 8sat/5K – track! OneBit 45 allocs: 2736 B 45 allocs: 2736 B
track/2. L1 8sat/5K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track-threaded 10 allocs: 4656 B 10 allocs: 4656 B
track/3. E1B 4sat/25K – track 10 allocs: 3056 B 10 allocs: 3056 B
track/3. E1B 4sat/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track! Int16 5 allocs: 416 B 5 allocs: 416 B
track/3. E1B 4sat/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track-threaded 10 allocs: 3056 B 10 allocs: 3056 B
track/4. 8L1+8E1B/25K – track 26 allocs: 10464 B 26 allocs: 10464 B
track/4. 8L1+8E1B/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track-threaded 26 allocs: 10464 B 26 allocs: 10464 B
track/5. multi-signal N=1/5K – track 9 allocs: 944 B 9 allocs: 944 B
track/5. multi-signal N=1/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/6. multi-signal N=2/5K – track 9 allocs: 1408 B 9 allocs: 1408 B
track/6. multi-signal N=2/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/7. multi-signal N=3/5K – track 9 allocs: 1760 B 9 allocs: 1760 B
track/7. multi-signal N=3/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/8. L1CA presync 2 blk – track! 7 allocs: 320 B 7 allocs: 320 B
track/8. L1CA presync 20 blk – track! 7 allocs: 320 B 7 allocs: 320 B
track/8. L1CA synced 2 blk – track! 7 allocs: 320 B 7 allocs: 320 B
track/8. L1CA synced 20 blk – track! 7 allocs: 320 B 7 allocs: 320 B
time_to_load 196 allocs: 13984 B 196 allocs: 13984 B

@zsoerenm zsoerenm changed the title Fix #198: measure track! completion path in benchmark; correct threaded @batch residual docs Document the threaded @batch per-block allocation and its root cause (#198) Jul 9, 2026
…ts root cause (#198)

Investigating #198 (track! "allocates per completed integration") found the
reported ~40 KB/completion to be a measurement artifact — `@allocated
track!(rand(ComplexF32, n), …)` counts the `rand` input buffer (n×8 B = 40 KB
per 5000-sample code period), which grows with signal length. With the buffer
hoisted out and measured from a typed call site, track! is flat: the
single-threaded backend is 0 B at any completion count (pinned by the existing
pre-sync allocation guard at 2 and 20 blocks, on top of #197 which removes the
_buffer_find_bit Core.Box that otherwise leaks ~80 B/block under a
multi-threaded runtime).

The one genuine residual is on the threaded backend: ~64 B per completed code
block per group, but only with >1 thread AND >1 satellite, since track!
launches Polyester's @Batch once per code block. Correct the docstrings that
mislabeled this as an "irreducible ~160 B per system per call" cost, and
document the actual root cause: Polyester's @Batch is allocation-free only when
the parallel closure touches nothing but Arrays and isbits values; it pays a
small per-launch allocation to root any other non-isbits object. The culprit is
the GNSS signal (e.g. GPSL1CA is not isbits — it wraps a Matrix code table and a
non-isbits SignalLUT), which each satellite's code-replica generation must
touch. The only way to remove it is to move code generation to a serial
pre-pass (allocation-free but serializes ~30% of the work and slows the threaded
pipeline), which is not worth ~64 B; the thread-over-PRNs design is kept.

Also cross-reference #198 in the pre-sync allocation guard's rationale (it
already pins allocation flat across completion counts). The track! benchmark
size is intentionally left at 2000 samples.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zsoerenm
zsoerenm merged commit 687157e into master Jul 9, 2026
12 checks passed
@zsoerenm
zsoerenm deleted the fix-198 branch July 9, 2026 08:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

track! post-sync per-integration allocation (~1.9 KB/completion/sat); original repro was measuring rand() — corrected

1 participant